Acknowledgement

How does reactivity work?

ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
  output$greeting <- renderText({
    paste0("Hello ", input$name, "!")
  })
}
shinyApp(ui, server)
  • Shiny performs the renderText() action every time we update input$name (automatically!)
  • reactive refers to any expression that automatically updates itself when its dependencies change

Recipe

  • Code within renderText({}) informs Shiny how it could create the string if it needs to, but it’s up to Shiny when (and even if!) the code should be run
  • Recipe: App provides Shiny with recipe (not commands) what to do with inputs

Imperative vs. Declarative programming and laziness

  • Imperative vs. declarative programming (Chapter 3.3.1)
    • Imperative code: “Make me a sandwich” (“assertive” code)
    • Declarative code: “Ensure there is a sandwich in the refrigerator whenever I look inside of it” (“passive-aggressive” code)
    • Shiny follows the latter principles
  • Laziness as strength of declarative programming (Chapter 3.3.2)
    • app will only ever do the minimal amount of work needed to update the output controls that you can currently see

The reactive graph

  • Usually R code can be read from top to bottom (= order of execution)… not in Shiny!
  • Reactive graph: describes how inputs/outputs are connected to understand the order of execution

  • output$greeting will need to be recomputed whenever input$name is changed
  • greeting has a reactive dependency on name

Reactive expressions

  • Reactive expressions take inputs and produce outputs
    • can reduce duplication in reactive code by introducing additional nodes into reactive graph
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
)

server <- function(input, output, session) {
  string <- reactive(paste0("Hello ", input$name, "!"))
  
  output$greeting <- renderText(string())
}
shinyApp(ui, server)

  • A reactive expression is drawn with angles on both sides because it connects inputs to outputs.

Reactive functions

Shiny provides a variety of reactive functions such as reactive(), observe(), bindevent() , render*(), etc.

Reactive expressions (cont’d)

  • Like inputs, you can use the results of a reactive expression in an output
  • Like outputs, reactive expressions depend on inputs and automatically know when they need updating
  • Reactive expressions are both producers and consumers

Executation order

  • The order Shiny code is run is solely determined by the reactive graph
  • We can flip the below code in the server function and it still works
    • But better keep the order for easier understanding!
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting"),
)

server <- function(input, output, session) {
  output$greeting <- renderText(string())
  string <- reactive(paste0("Hello ", input$name, "!"))
}
shinyApp(ui, server)

Reactive elements in our first app

demos/demo01.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

ui = fluidPage(
  titlePanel("Temperatures at Major Airports"),
  sidebarLayout(
    sidebarPanel(
      radioButtons(
        "name", "Select an airport",
        choices = c(
          "Seattle-Tacoma",
          "Raleigh-Durham",
          "Houston Intercontinental",
          "Denver",
          "Los Angeles",
          "John F. Kennedy"
        )
      ) 
    ),
    mainPanel( 
      plotOutput("plot")
    )
  )
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(aes(x=date, y=temp_avg)) +
      geom_line() +
      theme_minimal()
  })
}

shinyApp(ui = ui, server = server)

Our inputs and outputs are defined by the elements in our UI.

Reactive graph of our first app

demos/demo01.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

ui = fluidPage(
  titlePanel("Temperatures at Major Airports"),
  sidebarLayout(
    sidebarPanel(
      radioButtons(
        "name", "Select an airport",
        choices = c(
          "Seattle-Tacoma",
          "Raleigh-Durham",
          "Houston Intercontinental",
          "Denver",
          "Los Angeles",
          "John F. Kennedy"
        )
      ) 
    ),
    mainPanel( 
      plotOutput("plot")
    )
  )
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(aes(x=date, y=temp_avg)) +
      geom_line() +
      theme_minimal()
  })
}

shinyApp(ui = ui, server = server)

The “reactive” logic is defined in our server function - Shiny takes care of figuring out what depends on what.

Demo 02 - Adding an input

demos/demo02.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

d_vars <- c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press")

ui <- fluidPage(
  titlePanel("Weather Data"),
  sidebarLayout(
    sidebarPanel(
      radioButtons(
        "name", "Select an airport",
        choices = c(
          "Seattle-Tacoma",
          "Raleigh-Durham",
          "Houston Intercontinental",
          "Denver",
          "Los Angeles",
          "John F. Kennedy"
        )
      ),
      selectInput(
        "var", "Select a variable",
        choices = d_vars, selected = "temp_avg"
      )
    ),
    mainPanel( 
      plotOutput("plot")
    )
  )
)

server <- function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      theme_minimal()
  })
}

shinyApp(ui = ui, server = server)

Tidy evaluation: data-variable vs. env-variable

  • One aspect of the improved code that you might be unfamiliar with is the use of .data[[input$var]] within renderPlot().

  • How to create Shiny apps that lets the user choose which variables will be fed into tidyverse functions like dplyr::filter() and ggplot2::aes().

  • There is a problem of indirection - we have a data-variable stored inside an env-variable (input$var)

    • An env-variable (environment variable) is a “programming” variables that you create with <-.
    • A data-variable (data frame variables) is “statistical” variable that lives inside a data frame.
  • .data[[input$var]] is a way to tell tidyverse functions to look inside the data frame for a variable whose name is stored in input$var.

Reactive graph - Demo 2

With these additions, what should our reactive graph look like now?

Your turn - Exercise 03

Starting with the code in exercises/ex03.R (based on demo02.R’s code) add a tableOutput() with id minmax to the app’s mainPanel().

Once you have done that you should then add logic to the server function to render a table that shows the min and max temperature for each year contained in these data.

12:00

Reactive graph - Exercise 03

With these additions, what should our reactive graph look like now?

reactlog

Another (more detailed) way of seeing the reactive graph (dynamically) for your app is using the reactlog package.

Run the following to log and show all of the reactive events occurring within ex03_soln.R,


reactlog::reactlog_enable()

(source(here::here("exercises/solutions/ex03_soln.R")))

shiny::reactlogShow()

# to reset the log
reactlog::reactlog_reset()

Demo 03 - Don’t repeat yourself

demos/demo03.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

d_vars = c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press",
           "Total sunshine" = "total_sun")

ui = fluidPage(
  titlePanel("Weather Data"),
  sidebarLayout(
    sidebarPanel(
      radioButtons(
        "name", "Select an airport",
        choices = c(
          "Raleigh-Durham",
          "Houston Intercontinental",
          "Denver",
          "Los Angeles",
          "John F. Kennedy"
        )
      ),
      selectInput(
        "var", "Select a variable",
        choices = d_vars, selected = "temp_avg"
      )
    ),
    mainPanel( 
      plotOutput("plot"),
      tableOutput("minmax")
    )
  )
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      theme_minimal()
  })
  
  output$minmax = renderTable({
    d |> 
      filter(name %in% input$name) |>
      mutate(
        year = lubridate::year(date) |> as.integer()
      ) |>
      summarize(
        `min temp` = min(temp_min),
        `max temp` = max(temp_max),
        .by = year
      )
  })
}

shinyApp(ui = ui, server = server)

Demo 04 - Enter reactive

demos/demo04.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

d_vars = c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press")

ui = fluidPage(
  titlePanel("Weather Data"),
  sidebarLayout(
    sidebarPanel(
      radioButtons(
        "name", "Select an airport",
        choices = c(
          "Seattle-Tacoma",
          "Raleigh-Durham",
          "Houston Intercontinental",
          "Denver",
          "Los Angeles",
          "John F. Kennedy"
        )
      ),
      selectInput(
        "var", "Select a variable",
        choices = d_vars, selected = "temp_avg"
      )
    ),
    mainPanel( 
      plotOutput("plot"),
      tableOutput("minmax")
    )
  )
)


server = function(input, output, session) {
  d_city = reactive({
    d |>
      filter(name %in% input$name)
  })
  
  output$plot = renderPlot({
    d_city() |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      ggtitle(names(d_vars)[d_vars==input$var]) +
      geom_line() +
      theme_minimal()
  })
  
  output$minmax = renderTable({
    d_city() |>
      mutate(
        year = lubridate::year(date) |> as.integer()
      ) |>
      summarize(
        `min temp` = min(temp_min),
        `max temp` = max(temp_max),
        .by = year
      )
  })
}

shinyApp(ui = ui, server = server)

reactive() tips

  • Expressions are written in the same way as render*() functions

  • If react_obj = reactive({...}) then any consumer must access value using react_obj() and not react_obj

  • Like input reactive expressions may only be used within reactive contexts (e.g. render*(), reactive(), observer(), etc.)

  • Their primary use is similar to a function in an R script, they help to

    • avoid repeating ourselves

    • decompose complex computations into smaller / more modular steps

    • improve computational efficiency by breaking up / simplifying reactive dependencies

Reactive graph - Demo 4

With these additions, what should our reactive graph look like now?

observer()

  • These are constructed in the same way as a reactive() however an observer does not return a value, instead they are used for their side effects.

    • The side effects in most cases involve sending data to the client broswer, e.g. updating a UI element

    • While not obvious given their syntax - the results of the render*() functions are observers.

Demo 05 - Filtering by region

demos/demo05.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("data/weather.csv"))

d_vars = c("Average temp" = "temp_avg",
           "Min temp" = "temp_min",
           "Max temp" = "temp_max",
           "Total precip" = "precip",
           "Snow depth" = "snow",
           "Wind direction" = "wind_direction",
           "Wind speed" = "wind_speed",
           "Air pressure" = "air_press")

ui = fluidPage(
  titlePanel("Weather Data"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "region", "Select a region", 
        choices = c("West", "Midwest", "Northeast", "South")
      ),
      selectInput(
        "name", "Select an airport", choices = c()
      ),
      selectInput(
        "var", "Select a variable",
        choices = d_vars, selected = "temp_avg"
      )
    ),
    mainPanel( 
      plotOutput("plot"),
      tableOutput("minmax")
    )
  )
)

server = function(input, output, session) {
  observe({
    updateSelectInput(
      session, "name",
      choices = d |>
        distinct(region, name) |>
        filter(region == input$region) |>
        pull(name)
    )
  })
  
  d_city = reactive({
    d |>
      filter(name %in% input$name)
  })
  
  output$plot = renderPlot({
    d_city() |>
      ggplot(aes(x=date, y=.data[[input$var]])) +
      ggtitle(names(d_vars)[d_vars==input$var]) +
      geom_line() +
      theme_minimal()
  })
  
  output$minmax = renderTable({
    d_city() |>
      mutate(
        year = lubridate::year(date) |> as.integer()
      ) |>
      summarize(
        `min temp` = min(temp_min),
        `max temp` = max(temp_max),
        .by = year
      )
  })
}

shinyApp(ui = ui, server = server)

Reactive graph - Demo 5

With these additions, what should our reactive graph look like now?

Using req()

  • You may have notices that the App initialises with “West” selected for region but no initial selection for name because of this we have some warnings generated in the console:
Warning: There were 2 warnings in `summarize()`.
The first warning was:
ℹ In argument: `min temp = min(temp_min)`.
Caused by warning in `min()`:
! no non-missing arguments to min; returning Inf
ℹ Run dplyr::last_dplyr_warnings() to see the 1 remaining warning.
  • This can be a common occurrence with Shiny, particularly at initialisation or when a user enters bad / unexpected input(s).

  • A good way to protect against this is to validate your inputs - the simplest way is to use req() which checks if a value is truthy.

  • Non-truthy values prevent further execution of the reactive code (and downstream consumer’s code).

Your turn - Exercise 04

  • Using the code provided in exercise/ex04.R (based on demo/demo05.R) as a starting point add the calls to req() necessary to avoid the initialisation warnings.

  • Also, think about if there are any other locations in our app where req() might be useful.

Hint - thinking about how events “flow” through the reactive graph will be helpful here.

10:00